from datetime import datetime

import requests
from JSONLibrary import JSONLibrary
from robot.api.deco import keyword
from SeleniumLibrary.keywords import BrowserManagementKeywords

import json
from robot.libraries.BuiltIn import BuiltIn
from robot.result.model import TestCase as TestCaseResult
from robot.running.model import TestCase as TestCase

from Lib.zcm.assistant.FwVariables import get_ignore_error
from Lib.zcm.utils.ui_proxy import is_intercept_ui_request as is_intercept_ui_request
from Lib.zcm.utils.ui_proxy import new_har as new_har
from Lib.zcm.utils.ui_proxy import proxy_close as proxy_close
from Lib.zcm.utils.ui_proxy import get_har as get_har
from Lib.zcm.utils.ui_proxy import proxy_add_header as proxy_add_header

from Lib.zcm.assistant.ExcelOpts import ExcelOpts
from Lib.zcm.exception.handler import exception_handler
from Lib.zcm.portal.Common import Common
from Lib.zcm.portal.portallogin import portal_open_menu, change_portal
from Lib.zcm.utils.LazyImport import *
from Lib.zcm.utils.dubbo_client import Dubbo_Client, Dubbo_Test_Client
from Lib.zcm.utils.json_encoder import DecimalEncoder
from Lib.zcm.utils.pod_controller import Pod_Controller
from Lib.zcm.utils.screenshot import save_html_file
from Lib.zcm.utils.wrapper import *
from Lib.zcm.portal.CardInfoByExcel import CardInfoByExcel
from Lib.zcm.portal.CardInfoByTxt import CardInfoByTxt
from Lib.zcm.fish.alert import process_fish_modal_dialog_alert, check_page_error, check_page_error_before_operate, \
    check_page_error_after_operate
from Lib.zcm.assistant.FwCSVOpts import FwCSVOpts
from Lib.zcm.utils.http_interface import http_interface_obj
from Lib.zcm.utils.socket_operation import SocketOperation
from Lib.zcm.utils.safety_monitor import SafetyMonitor
from Lib.constants import ZCM_PORTAL_ROOT_URL
from Lib.zcm.utils.ui_proxy import proxy_add_driver_options as proxy_add_driver_options
from Lib.zcm.utils.ui_proxy import get_remote_url
from Lib.zcm.utils.sftp_utils import sftp_obj, execute_remote_command


class ZCM(SeleniumLibrary):
    ROBOT_LIBRARY_SCOPE = "GLOBAL"
    ROBOT_LIBRARY_DOC_FORMAT = "ROBOT"

    ROBOT_LISTENER_API_VERSION = 3

    def __init__(self, timeout=5.0, implicit_wait=0.0, run_on_failure='Capture Page Screenshot',
                 screenshot_root_directory=None, plugins=None, event_firing_webdriver=None):
        super().__init__(timeout, implicit_wait, run_on_failure, screenshot_root_directory, plugins,
                         event_firing_webdriver)
        # 如果是Itracing方式采集覆盖率，则统一注入header
        if FwVariables.is_open_itracing_code_cover_collector():
            self.add_http_proxy_header()
        self.ROBOT_LIBRARY_LISTENER = self

    def _start_test(self, case: TestCase, result: TestCaseResult):
        # switch 打开 然后去判断 mode
        if is_intercept_ui_request():
            # 用例开始时，打开代理
            new_har(case.name, options={'captureHeaders': True})
        # 如果是单用例采集且是通过Itracing方式采集覆盖率信息
        if FwVariables.is_open_collect_single_case_code_cover() and FwVariables.is_open_itracing_code_cover_collector():
            self.add_http_proxy_header(case)

    def _end_test(self, case: TestCase, result: TestCaseResult):
        if is_intercept_ui_request():
            # 用例结束，打印拦截的异常请求
            self.log_har(get_har(), result, FwVariables.ui_request_cost_thread())

    def _close(self):
        proxy_close()

    @keyword(name="ZOpenBrowser")
    @exception_handler()
    @capture_screen_handler()
    def openBrowser(self, url, options=None, **kwargs):
        """
        打开浏览器并进入指定url
                Examples:
                | ||
        :param url:
        :param options: 设置driver参数，比如chromedriver 中文 add_argument('lang=zh_CN.UTF-8') 英文  add_argument('lang=en_US')
        :return:
        """
        logger.debug("login {0}".format(url))
        browser_management = BrowserManagementKeywords(self)

        if options is None:
            logger.debug("use zcm proxy in browser")
            options = 'add_argument(\'log-level=3\')'
        else:
            options = options + '; add_argument(\'log-level=3\')'
        options = proxy_add_driver_options(options, url=url)
        logger.debug("login {0} options {1}".format(url, options))
        browser_management.open_browser(url, 'chrome', options=options, remote_url=get_remote_url(), **kwargs)
        # self._drivers.current.set_script_timeout(60)
        # self._drivers.current.implicitly_wait(60)
        self._drivers.current.maximize_window()
        BuiltIn().set_suite_variable('${commonWebDriver}', self._drivers.current)

    @keyword(name="ZLogin")
    @exception_handler()
    @capture_screen_handler()
    def login(self, url=None, username=None, password=None, options=None, wait_seconds=0, version=None):
        """
        login portal. default username is admin, passwod is 11.
                Examples:
                | ZLogin | http://10.45.80.3 | admin | 11 |
                | ZLogin | http://10.45.80.3 |
        :param url:
        :param username:
        :param password:
        :param options: 设置driver参数，比如chromedriver 中文 add_argument('lang=zh_CN.UTF-8') 英文  add_argument('lang=en_US')
        :param wait_seconds: 页面打开后，需再等待多少秒后，再进行登录操作，默认为0
        :param version: portal版本
        :return:
        """
        login_info = Common().get_login_info(url=url, user=username, password=password)
        logger.info("version:{}".format(version))
        options = proxy_add_driver_options(options, url=login_info['url'])
        logger.info("{0}".format(options))
        browser_management = BrowserManagementKeywords(self)
        try_num = 5
        while try_num:
            try:
                browser_management.open_browser(login_info['url'], FwVariables.get_driver(), options=options,
                                                remote_url=get_remote_url())
                break
            except Exception as e:
                if try_num > 1:
                    logger.warn("open browser failed! try again!")
                else:
                    raise Exception(traceback.format_exc())
                time.sleep(2)
                try_num -= 1
        # self._drivers.current.set_script_timeout(60)
        # self._drivers.current.implicitly_wait(60)
        self._drivers.current.maximize_window()
        # logger.debug('login_info {0}'.format(login_info))
        # Add by james at 20210514
        # For pto项目，打开登录页面后，会重新刷一遍页面，所以需要等待一下
        wait = int(wait_seconds)
        if wait > 0:
            logger.debug(f"Waiting {wait}s before login operation")
            time.sleep(wait)
        Common().login(self._drivers.current, login_info['url'], login_info['user'], login_info['pwd'], version)
        BuiltIn().set_suite_variable('${commonWebDriver}', self._drivers.current)

    @keyword(name="ZOpenMenu")
    @exception_handler()
    @capture_screen_handler()
    def open_menu(self, *menus):
        """
        打开menu
                Examples:
                | ZOpenMenu | CRM Customer Delegate Portal | CRM Order Entry | CRM Order Entry |
        :param portal_name:
        :param menus:
        :return:
        """
        # with WaitPageLoad(self._drivers.current):
        if not portal_open_menu(self._drivers.current, *menus):
            Common().openMenu(self._drivers.current, *menus)

    @keyword(name="ZCheckPageError")
    @exception_handler()
    @capture_screen_handler()
    def check_page(self):
        """
        检查页面是否正常打开
        Examples:
        | ZCheckPageError |

        return: 页面正常返回True，页面错误返回False
        """
        with WaitPageLoad(self._drivers.current):
            return check_page_error(self)

    @keyword(name="ZCheckMenu")
    @exception_handler()
    @capture_screen_handler()
    def check_menu(self, url, username, password, version=None):
        """
        检测portal中所有菜单是否能够正常打开

        Examples:
        | ${result} | ZCheckMenu | http://10.45.6.147:7199/portal/ | admin | Flying@123 |
        | #ZCheckMenuResult | ${result} |
        :param url: 登陆地址
        :param username: 用户名
        :param password: 密码
        :param version: portal版本，可不传，不传则从config.ini中读取
        :return: tuple，包含正确打开的页面信息，错误打开的信息
        """
        return Common(version).check_menu(self, url, username, password, version)

    @keyword(name="ZCheckMenuResult")
    @exception_handler()
    @capture_screen_handler()
    def check_menu_result(self, result):
        """
        分析检查结果，有异常则报错

        Examples:
        | ${result} | ZCheckMenu | http://10.45.6.147:7199/portal/ | admin | Flying@123 |
        | #ZCheckMenuResult | ${result} |
        :param result: 检查结果
        :return: 全部正常返回True，否则报错失败
        """
        error_page_info = result[1]
        if not error_page_info:
            logger.info("所有菜单正常打开")
            return True
        logger.info("\nerror_page_info:\nportal\t\turl\t一级菜单\t二级菜单\t三级菜单\t四级菜单\n{}".format("\n").join(error_page_info))
        raise Exception("有菜单未正常打开！请查看日志！")

    @keyword(name="ZChangePortal")
    @exception_handler()
    @capture_screen_handler()
    def change_portal(self, portal_name):
        """
        从右侧菜单切换门户功能
                Examples:
                | ZChangePortal | 平台管理门户 |
        :param portal_name: 门户的菜单的名称
        :return:
        """
        change_portal(self._drivers.current, portal_name)

    @keyword(name="ZCloseBrowser")
    @exception_handler()
    def close_browser(self):
        """
        关闭Browser的一个tab页，不清除后台的浏览器驱动
                Examples:
                | ZCloseBrowser | ${commonWebDriver} |
        :return:
        """
        browser_management = BrowserManagementKeywords(self)
        browser_management.close_browser()
        # Common().closeBrowser(self._drivers.current)

    @keyword(name="ZQuitBrowser")
    @exception_handler()
    def quit_browser(self):
        """
        关闭所有Browser，清除后台的浏览器驱动
                Examples:
                | ZQuitBrowser | ${commonWebDriver} |
        :return:
        """
        Common().quitBrowser(self._drivers.current)

    @keyword(name="ZClick")
    @exception_handler()
    @capture_screen_handler()
    def click(self, element, *param):
        """
        点击页面元素
                Examples:
                | ZClick | ${page.menu_account_info()} | | | |
                | ZClick | locator_string | | | |
                | ZClick | canvas_locator_string | 50 | 50 | # 对于canvas画布元素，根据相对元素位置的百分比点击，比如 50 50 就是元素的中间 |
        :param element: 注意： 如果是 locator_string
        :param param:
        :return:
        """
        check_page_error_before_operate(self)
        # 适配某个点击过程需要等待超长多个BlockUI情况
        with WaitPageLoad(self._drivers.current):
            # 通过selector 转成组件
            if isinstance(element, str):
                element = Common().get_element_by_selector(self._drivers.current, element)

            if param:
                element.click(*param)
            else:
                element.click()
        check_page_error_after_operate(self)

    @keyword(name="ZGetLog")
    @exception_handler()
    @capture_screen_handler()
    def get_browser_log(self, log_type):
        """
        根据参数查询日志，browser：输出chrome控制台的各种日志信息
        Examples:
        ${browser_log}    ZGetLog
        Log    ${browser_log}
        param log_type:
        return：
        """
        browser_log = self._drivers.current.get_log(log_type)
        return browser_log

    @keyword(name="ZRightClick")
    @exception_handler()
    @capture_screen_handler()
    def right_click(self, element):
        """
        页面元素点击右键
                Examples:
                | ZRightClick | ${page.menu_account_info()} | | | |
                | ZRightClick | locator_string | | | |
        :param element:
        :return:
        """
        # 适配某个点击过程需要等待超长多个BlockUI情况
        with WaitPageLoad(self._drivers.current):
            # 通过selector 转成组件
            if isinstance(element, str):
                element = Common().get_element_by_selector(self._drivers.current, element)

            action = ActionChains(self._drivers.current)
            ele = element._get_web_element(True)
            action.context_click(ele).perform()

    @keyword(name="ZDoubleClick")
    @exception_handler()
    @capture_screen_handler()
    def double_click(self, element, *param):
        """
        点击页面元素
                Examples:
                | ZDoubleClick | ${page.menu_account_info()} | | | |
                | ZDoubleClick | locator_string | | | |
                | ZDoubleClick | canvas_locator_string | 50 | 50 | # 对于canvas画布元素，根据相对元素位置的百分比点击，比如 50 50 就是元素的中间 |
        :param element: 注意： 如果是 locator_string
        :param param:
        :return:
        """
        check_page_error_before_operate(self)
        # 适配某个点击过程需要等待超长多个BlockUI情况
        with WaitPageLoad(self._drivers.current):
            # 通过selector 转成组件
            if isinstance(element, str):
                element = Common().get_element_by_selector(self._drivers.current, element)

            if param:
                element.doubleClick(*param)
            else:
                element.doubleClick()
        check_page_error_after_operate(self)

    @keyword(name="ZClickMulBlock")
    @exception_handler()
    @capture_screen_handler()
    def click_wait_mulity_blockUI(self, element, interval=None, times=None):
        interval = interval if interval else FwVariables.get_block_interval()
        times = times if times else FwVariables.get_block_times()
        with WaitPageLoad(self._drivers.current, interval=interval, times=times):
            element.click()

    @keyword(name="ZSetValue")
    @exception_handler()
    @capture_screen_handler()
    def set_value(self, element, *param):
        """
        set values,including combobox、input、datetime、checkbox、switch button、tabs、radio...
                | ZSetValue | custName   | autoTest |
                | ZSetValue | impGradeId | VIP |
                | zSetValue | locator_string | value | #输入框 |
                | zSetValue | locator_string | 2020-09-21,2020-09-22 | #日期 |
                | zSetValue | locator_string | ON | #开关，支持ON,开，启用，OFF，关，禁用，如不带值，则切换一次 |
                | zSetValue | locator_string | value1 | value2 | # check box |
                | zSetValue | locator_string | file_path_name | # 上传文件，只针对react组件，会尝试判断是否上传完成 |
        :param element:
        :param param:
        :return:
        """
        check_page_error_before_operate(self)
        with WaitPageLoad(self._drivers.current):
            # 通过selector 转成组件
            if isinstance(element, str):
                element = Common().get_element_by_selector(self._drivers.current, element)

            if param:
                element.setValue(*param)
            else:
                element.setValue()
        check_page_error_after_operate(self)

    @keyword(name="ZGetValue")
    @exception_handler()
    @capture_screen_handler()
    def get_value(self, element):
        """
        获取value值
                Examples:
                | ${result}= | getValue | CRMOrderEntryPage().customer_form_birthday()   |
                | ${result}= | getValue | locator_string |
        :param element:
        :return:
        """
        with WaitPageLoad(self.driver):
            # 通过selector 转成组件
            if isinstance(element, str):
                element = Common().get_element_by_selector(self._drivers.current, element)

            return element.getValue()

    @keyword(name="ZCheckValue")
    @exception_handler()
    @capture_screen_handler()
    def check_value(self, element, *params):
        """
        检查元素的值，如果不相等则报异常
                Examples:
                | ZCheckValue | CRMOrderEntryPage().customer_form_birthday()   |  hello | | |
                | ZCheckValue | locator_string | hello | | # 检查元素的text是否与hello一致 |
                | ZCheckValue | table_locator_string | title_name | cell_value | # 检查表格中的title_name列中，是否有cell_value值 |
        :param element:
        :return:
        """
        with WaitPageLoad(self.driver):
            # 通过selector 转成组件
            if isinstance(element, str):
                element = Common().get_element_by_selector(self._drivers.current, element)

            element.checkValue(*params)

    @keyword(name="ZSelect")
    @exception_handler()
    @capture_screen_handler()
    def select(self, element, *param):
        """
        选中某个元素
                Examples:
                | ZSelect | CRMOrderEntryPage().customer_form_birthday() ||||
                | zSelect | locator_string ||||
                | zSelect | table_locator_string | title_name | cell_value | # 选择表格中的title_name列中，值为cell_value的那一行记录 |
        :param element:
        :param param:
        :return:
        """
        check_page_error_before_operate(self)
        with WaitPageLoad(self._drivers.current):
            # 通过selector 转成组件
            if isinstance(element, str):
                element = Common().get_element_by_selector(self._drivers.current, element)
            element.select(*param)
        check_page_error_after_operate(self)

    @keyword(name="ZInitPage")
    @exception_handler()
    def init_page(self, pageName):
        """
        实例化page
                Examples:
                | ZInitPage | CRMOrderEntryPage |
                | ZInitPage | CRMOrderEntryPage | AddPostAcctInfoPage |
        :param pageName:
        :return: 全局变量${CRMOrderEntryPage}、${AddPostAcctInfoPage}
        """
        page = Common().init_page(self._drivers.current, pageName)
        BuiltIn().set_test_variable('${}'.format(pageName), page)
        return page

    @keyword(name="ZWait")
    @exception_handler()
    @capture_screen_handler()
    def wait_element(self, element, timeout=None, type="presence"):
        """
        采用默认值等待元素可见，最大时间限制10S，频率0.5s每次，recheck=True
              Examples:
              | ZWait | elemet |
              | ZWait | locator_string |
        :param element:
        :return:
        """
        assert type in ["ready", "presence", "visible"], "Type error."
        try:
            # 通过selector 转成组件
            if isinstance(element, str):
                # element = Common().get_element_by_selector(self._drivers.current, element)
                if element.upper().startswith('P='):
                    from Lib.zcm.assistant.page_config import page_parser
                    element = page_parser.read(element[2:])
                from Lib.zcm.utils import parse_locator_str
                selector_type, selector = parse_locator_str(element)
                element = PageElement(self._drivers.current, selector, selector_type)

            with WaitPageLoad(self._drivers.current):
                func = getattr(element, "wait_element_{}".format(type))
                logger.info(func)
                if timeout:
                    func(timeout=int(timeout))
                else:
                    func()
        except Exception as e:
            logger.error(e)
            # logger.error("Element not appeared!")
            raise TestException.by_error_code(ErrorCode.ELEMENT_TIME_OUT, message="Element not appeared!.")

    @keyword(name="ZCallFunction")
    @exception_handler()
    @capture_screen_handler()
    def call_class_function(self, module_path, class_name: str, function_name: str, *_) -> None:
        """
        动态执行某个类的某个方法.默认类的初始化只有一个参数,也就是self._drivers.current
        Examples:
            |ZCallFunction|V81E.CRM.Page.CRMOrderEntry.CRMOrderEntry.AddCustomerPage|AddCustomerPage|add_customer|json str
        :param module_path: 类的模块路径
        :param class_name:  类名
        :param function_name:方法名
        :param _: 调用方法的时候需要的参数,可变参数
        :return: None 这里后续要优化一下
        """
        import_module = LazyImport(module_path, class_name)
        is_true = hasattr(import_module, class_name)
        if is_true:
            class_name = getattr(import_module, class_name)
            page_instance = class_name(self._drivers.current)
            function = getattr(page_instance, function_name)
            return function(*_)

    @keyword(name="ZDBQuery")
    @exception_handler()
    def db_query(self, link_name, selectStatement, db_type="Oracle"):
        """数据库查询
        Examples:
        | ${str} | ZDbQuery | 81E_ocs_master | SELECT * FROM order_item; |
        :param link_name: config.ini中配置的数据section名称
        :param selectStatement: 查询语句
        :return: 数据列表，如果取第一条数据的第一列值 ${str}[0][0]
        """
        from Lib.zcm.database.DBOperate import ad_operation
        return ad_operation.db_query(link_name, selectStatement, db_type)

    @keyword(name="ZDBQueryOne")
    @exception_handler()
    def db_query_one(self, link_name, selectStatement, db_type="Oracle"):
        """数据库查询,返回第一条记录,如果没找到记录，返回空数据
        Examples:
        | ${str} | ZDBQueryOne | 81E_ocs_master | SELECT * FROM order_item where id=2; |
        :param link_name: config.ini中配置的数据section名称
        :param selectStatement: 查询语句，
        :return: 数据列表，如果取第一条数据的第一列值 ${str}[0][0]
        """
        from Lib.zcm.database.DBOperate import ad_operation
        return ad_operation.db_query_one(link_name, selectStatement, db_type)

    @keyword(name="ZDBQueryOneValue")
    @exception_handler()
    def db_query_one_value(self, link_name, selectStatement, db_type="Oracle"):
        """数据库查询,返回第一条记录中的第一个字段,如果没找到记录，返回空(None)
        Examples:
        | ${str} | ZDBQueryOneValue | 81E_ocs_master | SELECT name FROM order_item where id=2; |
        :param link_name: config.ini中配置的数据section名称
        :param selectStatement: 查询语句
        :return: 数据列表，如果取第一条数据的第一列值 ${str}[0][0]
        """
        from Lib.zcm.database.DBOperate import ad_operation
        return ad_operation.db_query_one_value(link_name, selectStatement, db_type)

    @keyword(name="ZDBExeSql")
    @exception_handler()
    def db_exe_sql_string(self, link_name, sqlString, db_type="Oracle"):
        """
        数据库查询
                Examples:
                | ZDBExeSql | 81E_ocs_master | update order_tiem set name='hello' where id=1; |
        :param dbName: config.ini中配置的数据section名称
        :param sqlString: 查询语句
        :return:
        """
        from Lib.zcm.database.DBOperate import ad_operation
        return ad_operation.db_exe_sql_string(link_name, sqlString, db_type)

    @keyword(name="ZDBExeSqlFile")
    def db_exe_sql_script(self, link_name, sql_file):
        """执行数据库脚本文件

        Examples:
            | ZDBExeSqlFile | db_link | ${EXECDIR}${/}resources${/}DDL-setup.sql |

        - param db_link: config.ini中配置的数据section名称
        - sql_file: 数据库脚本路径

        SQL文件中，每个命令用sql分隔，如下.

        DELETE FROM person_employee_table;
        DELETE FROM person_table;
        DELETE FROM employee_table;
        """
        from Lib.zcm.database.DBOperate import ad_operation
        return ad_operation.db_exe_sql_script(link_name, sql_file)

    @keyword(name="ZCSVDropColumns")
    def csv_drop_columns(self, file_path, *title):
        """
        删除cvs文件中某一列
        :param file_path:
        :param title:
        :return:
        """
        try:
            FwCSVOpts(file_path).drop_columns(*title)
        except Exception as e:
            logger.info(e)

    @keyword(name="ZCSVGetColumn")
    def csv_get_column(self, file_path, title: str):
        """
        获取csv某一列的值
        :param file_path:
        :param title:
        :return: list
        """

        def deal_star(data):
            data = str(data) + "\t"
            return data

        df = FwCSVOpts(file_path)
        df = df._read_csv()
        df[title] = df[title].map(deal_star)
        return df[title].tolist()

    @keyword(name="ZCSVAddColumns")
    def csv_add_columns(self, file_path, **kwargs):
        """
        为csv文件追加一列
        :param file_path:
        :param kwargs: 键值对类型参数
        :return:
        """
        logger.info(kwargs)
        FwCSVOpts(file_path).add_columns(**kwargs)

    @keyword(name="ZCheckPort")
    @exception_handler()
    def check_port(self, ip, port):
        """
        检查目标主机端口是否打开
        param ip:主机ip，传字符串时，默认是单个ip。也可传入列表
        param port:传字符串时，默认是单个端口，也可传入列表
        Examples:
            ${ints}    Create List    52890    3306    8080
            ${result}    ZCheckPort    127.0.0.1    ${ints}
            Log    ${result}
        return: ip不通，返回{127.0.0.1: 0}。ip通，返回字典：{127.0.0.1: {52890: 1, 3306: 2, 8080: 2}}   // 1：ip可连接且端口已打开，2：ip可连接但端口未打开
        """
        return SocketOperation().check_address_port(ip, port)

    @keyword(name="ZHttpNeedSaveHeader")
    @exception_handler()
    def http_save_header(self, *header_names):
        """
        需要框架自动处理HTTP消息中的Head头, 接口消息会自动保存Head头，并在下次请求时自动带上，适用于需要登录鉴权的接口
        - header_names: header头名称
        Examples:
            | ZHttpNeedSaveHeader | AUTH-TOKEN | TOKEN |
        """
        self._http_headers_save = header_names

    @keyword(name="ZHttpGet")
    @exception_handler()
    def http_get(self, url, **params):
        """
        HTTP 发送GET消息
                Examples:
                | ${response} | ZHttpGet | http://127.0.0.1/test |  |
        - :param url:
        - :param params: 可选，发送的清求参数，字典类型，如{'key': 'value'}，get消息 http://url/?key=value
        - :param headers: 可选， header头，字典类型.
        - :param cookies: 可选， 字典或CookieJar类型，默认会自动给你处理，你也可以自己设置 .
        - :param auth: 可选，权益信息，tuple类型，包括 Basic/Digest/Custom HTTP Auth.
        - :param timeout: 可选，超时时间，单位秒，可以是tuple类型，（连接超时时间，获取数据超时时间）
        - :param allow_redirects: 是否允许跳转，默认是True.
        - :param proxies: 可选，代理信息 .
        - :param verify: 可选，是否检证TLS证书，如果是字符串，则表示使用TLS证书的路径，默认为True
        - :param stream: (optional) if ``False``, the response content will be immediately downloaded.
        - :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
        - *return*: 以encoding解析返回内容，会自动根据响应头部的字符编码进行解码

        注：
        - 获取HTTP消息响应码：${_HTTP_RESPONSE.status_code}
        - 获取响应消息的Header头中的Connection值：${_HTTP_RESPONSE.headers['Connection']}
        """
        request_cookie, request_headers = http_interface_obj._get_http_cookie_and_headers(params.pop('cookies', None),
                                                                                          params.pop('headers', None))
        request_headers = self.update_case_info_header_to(request_headers)
        logger.debug("request headers {}".format(request_headers))
        result = requests.get(url, cookies=request_cookie, headers=request_headers, **params)
        http_interface_obj._save_http_cookie_and_headers(result)
        return result.text

    @keyword(name="ZHttpPost")
    @exception_handler()
    def http_post(self, url, **params):
        """
        HTTP Post消息发送
                Examples:
                | ${params} | Create Dictionary | key | value |
                | ${response} | ZHttpPost | http://127.0.0.1/test | data=${params}  |
        - :param url:
        - :param params: 可选，发送的清求参数，字典类型，如{'key': 'value'}，get消息 http://url/?key=value
        - :param data: 可选，HTTP消息Body里的内容，字典，列表，字节
        - :param json: 可选，HTTP消息Body里的内容，一个Json对像。
        - :param files: 可选，要发送的文件信息，比如{'name': ('filename',fileobj,'content_type')}.
        - param headers: 可选， header头，字典类型.
        - :param cookies: 可选， 字典或CookieJar类型，默认会自动给你处理，你也可以自己设置 .
        - :param auth: 可选，权益信息，tuple类型，包括 Basic/Digest/Custom HTTP Auth.
        - :param timeout: 可选，超时时间，单位秒，可以是tuple类型，（连接超时时间，获取数据超时时间）
        - :param allow_redirects: 是否允许跳转，默认是True.
        - :param proxies: 可选，代理信息 .
        - :param verify: 可选，是否检证TLS证书，如果是字符串，则表示使用TLS证书的路径，默认为True
        - :param stream: (optional) if ``False``, the response content will be immediately downloaded.
        - :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
        - *return*: 以encoding解析返回内容，会自动根据响应头部的字符编码进行解码

        注：
        - 获取HTTP消息响应码：${_HTTP_RESPONSE.status_code}
        - 获取响应消息的Header头中的Connection值：${_HTTP_RESPONSE.headers['Connection']}
        """
        case_info_header = self.get_case_info_header()
        return http_interface_obj.http_proxy_post(url, case_info_header, **params)

    @keyword(name="ZHttpDelete")
    @exception_handler()
    def http_delete(self, url, **params):
        """
        HTTP Delete消息发送
                Examples:
                | ${response} | ZHttpDelete | http://127.0.0.1/test |

        参数可以参照```ZHttpPost```关键字
        """
        request_cookie, request_headers = http_interface_obj._get_http_cookie_and_headers(params.pop('cookies', None),
                                                                                          params.pop('headers', None))
        request_headers = self.update_case_info_header_to(request_headers)
        result = requests.delete(url, cookies=request_cookie, headers=request_headers, **params)
        http_interface_obj._save_http_cookie_and_headers(result)
        return result.text

    @keyword(name="ZHttpPut")
    @exception_handler()
    def http_put(self, url, **params):
        """
        HTTP Put消息发送
                Examples:
                | ${response} | ZHttpPut | http://127.0.0.1/test |

        参数可以参照```ZHttpPost```关键字
        """
        request_cookie, request_headers = http_interface_obj._get_http_cookie_and_headers(params.pop('cookies', None),
                                                                                          params.pop('headers', None))
        request_headers = self.update_case_info_header_to(request_headers)
        result = requests.put(url, cookies=request_cookie, headers=request_headers, **params)
        http_interface_obj._save_http_cookie_and_headers(result)
        print(str())
        return result.text

    @keyword(name="ZHttpPatch")
    @exception_handler()
    def http_patch(self, url, **params):
        """
        HTTP Patch消息发送
                Examples:
                | ${response} | ZHttpPatch | http://127.0.0.1/test |

        参数可以参照```ZHttpPost```关键字
        """
        request_cookie, request_headers = http_interface_obj._get_http_cookie_and_headers(params.pop('cookies', None),
                                                                                          params.pop('headers', None))
        request_headers = self.update_case_info_header_to(request_headers)
        result = requests.patch(url, cookies=request_cookie, headers=request_headers, **params)
        http_interface_obj._save_http_cookie_and_headers(result)
        return result.text

    @keyword(name="ZHttpHead")
    @exception_handler()
    def http_head(self, url, **params):
        """
        HTTP Head消息发送
                Examples:
                | ${response} | ZHttpHead | http://127.0.0.1/test |

        参数可以参照```ZHttpPost```关键字
        """
        request_cookie, request_headers = http_interface_obj._get_http_cookie_and_headers(params.pop('cookies', None),
                                                                                          params.pop('headers', None))
        request_headers = self.update_case_info_header_to(request_headers)
        result = requests.head(url, cookies=request_cookie, headers=request_headers, **params)
        http_interface_obj._save_http_cookie_and_headers(result)
        return result.text

    @keyword(name="ZHttpOptions")
    @exception_handler()
    def http_options(self, url, **params):
        """
        HTTP Options消息发送
                Examples:
                | ${response} | ZHttpOptions | http://127.0.0.1/test |

        参数可以参照```ZHttpPost```关键字
        """
        request_cookie, request_headers = http_interface_obj._get_http_cookie_and_headers(params.pop('cookies', None),
                                                                                          params.pop('headers', None))
        request_headers = self.update_case_info_header_to(request_headers)
        result = requests.options(url, cookies=request_cookie, headers=request_headers, **params)
        http_interface_obj._save_http_cookie_and_headers(result)
        return result.text

    @keyword(name="ZScreenShot")
    @exception_handler()
    def screen_shot(self):
        """
        截屏 文件名默认为 zcm-screenshot-{index}.png，并插入到日志中
        :return:
        """
        logger.debug('just capture screen_shot')
        capture_page_screenshot(self._drivers.current)

    @keyword(name="ZSaveHtml")
    @exception_handler()
    def save_html(self):
        """
        保存当前html文件,文件名默认为 zcm-html-{index}.html
        :return:
        """
        return save_html_file(self._drivers.current)

    @keyword(name="ZGenCardInfo")
    @exception_handler()
    def gen_card_info(self, list_dict: list, path=None, model="EXCEL"):
        """
        向excel中填充卡的信息
        Examples:
        | ZGenCardInfoExcel | [{ "ICCID": "aaa", "IMSI": "123","KI":'1' },{ "ICCID": "bbb", "IMSI": "1111" }] | model=TXT |
        :param list_dict:
        :param model:
        :return:
        """
        type_list = ["EXCEL", "TXT"]
        assert model in type_list, "Type must be in type_list!"
        if path == None:
            if model == "EXCEL":
                CardInfoByExcel().write_to_excel(list_dict)
            elif model == "TXT":
                CardInfoByTxt().write_to_txt(list_dict)
        else:
            CardInfoByTxt().write_to_txt_by_path(list_dict, path)

    @keyword(name="ZGenCardInfoSP")
    @exception_handler()
    def get_card_info_sp(self, input_path, output_path, card_amount, dict: dict):
        """
        UM Starter Pack生成制卡文件，读取信息，将生成的文件增加卡信息，保存至out文件，支持多条批量操作
        Examples:
        | ZGenCardInfoSP | [{ "ICCID": "aaa", "IMSI": "123","KI":'1' },{ "ICCID": "bbb", "IMSI": "1111" }] | model=TXT |
        :param path:
        :param dict:
        :return:
        """
        return CardInfoByTxt().get_card_info_sp(input_path, output_path, card_amount, dict)

    @keyword(name="ZGenGoodsInfo")
    @exception_handler()
    def gen_goods_info(self, input_path, output_path, goods_info_list: list):
        """
        UM 生成实物文件，在当前txt文件后追加实物信息，写入文件，逗号隔开
        Examples:
        | ZGenGoodsInfo | [{'sku_code': '1101777', 'quantity': '1', 'start_goods_sn': 20210722001, 'condition_name': 'new'}, {'sku_code': '1101777', 'quantity': '1', 'start_goods_sn': 20210722002, 'condition_name': 'new'} | model=TXT |
        :param path:
        :param dict:
        :return:
        """
        return CardInfoByTxt().gen_goods_info(input_path, output_path, goods_info_list)

    @keyword(name="ZWriteDictToTXT")
    @exception_handler()
    def write_to_txt(self, input_path, output_path, dict: dict, separator=' '):
        """
        MTN上传文件适用，取文件信息，读取文件内容，换行，将dict value另起一行写入，以separator隔开
        Examples:
        | ZWriteDictToTXT | [{ "ICCID": "aaa", "IMSI": "123","KI":'1' },{ "ICCID": "bbb", "IMSI": "1111" }] | model=TXT |
        :param path:
        :param dict:
        :return:
        """
        CardInfoByTxt().write_to_txt_dict(input_path, output_path, dict, separator)

    @keyword(name="ZGenCardInfoDynamic")
    def generate_card_info(self, ICCID, IMSI, PIN1, PUK1, PIN2, PUK2, KI, ADM1=None, num=2):
        """
        动态生成卡信息
        Examples:
        | ${cont_card} | ZGenCardInfoDynamic | 8986020200914150000 | 850101300002001 | 2 |
        :param ICCID:
        :param IMSI:
        :param num:批量生成卡信息的数量
        :return:['{ "ICCID": "aaa", "IMSI": "123","KI":'1' }','{ "ICCID": "bbb", "IMSI": "1111" }']
        """
        return CardInfoByTxt().generate_card_info(ICCID, IMSI, PIN1, PUK1, PIN2, PUK2, KI, ADM1, num)

    @keyword(name="ZGetValueByReadTxt")
    def read_txt_by_key(self, filePath, key, sep, index):
        """
        获取txt文件中的内容
        Examples:
        | ${value} | ZGetValueByReadTxt | G:\\soft\\V9E2\\Case\\data\\uploadFile\\INP2021010043.out - Blank USIM.txt | IMSI | : |  ${1}  |
        :param filePath:
        :param key:
        :param sep:
        :param int:
        :return:
        """
        return CardInfoByTxt().get_content_by_keyword(filePath, key, sep, index)

    @keyword(name="ZGenDynamicStr")
    @exception_handler()
    def gen_current_time(self, prefix=None):
        '''
        以prefix为前缀，当前时间为后缀，拼接字符串
        :param prefix: 传入的字符串前缀
        :return: 拼接后的字符串，格式为prefix20200911163021
        '''
        dateStr = datetime.strftime(datetime.now(), '%Y%m%d%H%M%S')
        dyn_str = (prefix + dateStr) if prefix else dateStr
        return dyn_str

    @keyword(name="ZMoveCurse")
    @exception_handler()
    @capture_screen_handler()
    def move_curse(self, element):
        """
        移动光标元素位置
        Examples:
            | ZMoveCurse | ${AddProduct.helper_menu} |
            | ZMoveCurse | locator_string |
        :param element:
        :return:
        """
        with WaitPageLoad(self._drivers.current):
            # 通过selector 转成组件
            if isinstance(element, str):
                element = Common().get_element_by_selector(self._drivers.current, element)
            element.move_curse()

    @keyword(name="ZStep")
    @exception_handler()
    def step(self, content):
        """
        打印步骤信息
        Examples:
            | ZStep | 步骤信息 |
        :param content:
        :return:
        """
        logger.info("Step: {}".format(content))

    @keyword(name="ZFishAlertDialog")
    @exception_handler()
    @capture_screen_handler()
    def process_fish_modal_dialog_alert(self, button='OK', prompt_text=None):
        '''
         Fish Confirm, Info, Success, Prompt等弹框自动处理，默认选择OK
        Examples:
            | ZFishAlertDialog | OK |
            | ZFishAlertDialog | CANCEL | prompt_text |
            | ZFishAlertDialog | OK | prompt_text |
        :param button: 选择OK(确认),还是CANCEL(取消)
        :param prompt_text: Prompt对话框需要填入的数据
        :return:
        '''
        with WaitPageLoad(self._drivers.current):
            process_fish_modal_dialog_alert(self._drivers.current, button=button, prompt_text=prompt_text)

    @keyword(name="ZCheckElement")
    @exception_handler()
    @capture_screen_handler()
    def check_element_exist(self, element):
        '''
        判断某个页面元素是否存在
        Examples:
            | ${result} | ZCheckElement | ${CustomerView.order_num} |
            | ${result} | zCheckElement | locator_string |
        :param element:
        :return: 存在返回True， 不存在返回False
        '''
        with WaitPageLoad(self._drivers.current):
            # 通过selector 转成组件
            if isinstance(element, str):
                try:
                    element = Common().get_element_by_selector(self._drivers.current, element)
                except TestException as e:
                    logger.debug("check element failed", e)
                    return False

            if element.get_element_size() > 0:
                return True
            return False

    @keyword(name="ZImportVar")
    @exception_handler()
    def import_variables(self, suite_name: str, *args):
        """
        引入参数
        :param filename:
        :param args:
        :param overwrite:
        :return:
        """

        var_default = os.path.join(FwVariables.get_case_root(), 'data', 'variables',
                                   FwVariables.get_project_code_default(),
                                   (suite_name + ".py"))

        var_project = os.path.join(FwVariables.get_case_root(), 'data', 'variables',
                                   str(FwVariables.get_project_code()),
                                   (suite_name + ".py"))
        flag_default = os.path.exists(var_default)
        flag_project = os.path.exists(var_project)
        if not flag_default and not flag_project:
            raise TestException.by_error_code(ErrorCode.PROJECT_VARIABLE_PATH_NOT_EXIST, default_path=var_default,
                                              project_path=var_project)
            # raise Exception("Default and Project variable path not found! Please troubleshoot!")
        if flag_default:
            logger.info(f"import variable file {var_default}")
            BuiltIn().import_variables(eval(repr(var_default).replace('\\', '/')), *args)
        if flag_project:
            logger.info(f"import variable file {var_project}")
            BuiltIn().import_variables(eval(repr(var_project).replace('\\', '/')), *args)

    @keyword(name="ZImportRes")
    @exception_handler()
    def import_resource(self, suite_name: str):
        """
        引入参数
        :param filename:
        :param args:
        :param overwrite:
        :return:
        """

        var_default = os.path.join(FwVariables.get_case_root(), 'data', 'resource',
                                   FwVariables.get_project_code_default(),
                                   (suite_name + ".robot"))

        var_project = os.path.join(FwVariables.get_case_root(), 'data', 'resource', str(FwVariables.get_project_code()),
                                   (suite_name + ".robot"))
        flag_default = os.path.exists(var_default)
        flag_project = os.path.exists(var_project)
        if not flag_default and not flag_project:
            raise TestException.by_error_code(ErrorCode.PROJECT_RESOURCE_PATH_NOT_EXIST, default=var_default,
                                              project_path=var_project)
            # raise Exception("Default and Project resource path not found! Please troubleshoot!")
        if flag_default:
            BuiltIn().import_resource(eval(repr(var_default).replace('\\', '/')))
        else:
            logger.error("Default resource path not found! Please troubleshoot!")
        if flag_project:
            BuiltIn().import_resource(eval(repr(var_project).replace('\\', '/')))
        else:
            logger.error("Project resource path not found! Using the default resource file!")

    @keyword(name="ZIsContain")
    @exception_handler()
    def is_contain_str(self, src_str, expect_str):
        """
        判断字符串中是否包含某字符串
        Examples:
            | ${result} | ZIsContain | src_str | expect_str |
        :param src_str:  字符串
        :param expect_str: 字符串
        :return: 包含返回True，不包含返回False
        """
        flag = False
        if expect_str in src_str:
            flag = True
        return flag

    @keyword(name="ZGetMatchingCount")
    @exception_handler()
    def get_matching_count(self, element):
        """
        获取元素的个数，筛选结果数量
        Examples:
            | ${count} | ZGetMatchingCount | element |
        :param element:
        :return:
        """
        with WaitPageLoad(self._drivers.current):
            # 通过selector 转成组件
            if isinstance(element, str):
                element = Common().get_element_by_selector(self._drivers.current, element)
            return element.get_element_size()

    @keyword(name="ZCallPageFunc")
    @exception_handler()
    def call_page_func(self, page_instance, function_name, *args):
        """
        执行Page中封装的方法
        Examples:
            | ZCallPageFunc | page_instance | function_name | cust_name |
        :param page_instance:
        :param function_name:
        :param args:
        :return:
        """
        logger.info(args)
        function = getattr(page_instance, function_name)
        logger.info("Start executing page_func: {}".format(function_name))
        with WaitPageLoad(self._drivers.current):
            try:
                return function(*args)
            except Exception as e:
                raise Exception("function_name执行异常: {}".format(e))
            else:
                logger.info("End executing page_func: {}".format(function_name))

    @keyword(name="ZResultCheck")
    @exception_handler()
    def result_check(self, builtIn_func, first, second, err_msg=None, expect_msg=None):
        '''
        步骤结果打印
        Examples:
            | ZResultCheck | builtIn_func | page_instance | function_name | cust_name |
        :param builtIn_func: should_be_equal、should_not_be_equal、should_not_be_equal_as_integers、should_contain等
        :param first: 实际值
        :param second: 期望值
        :param err_msg: 实际结果打印
        :param expect_msg: 期望结果打印
        :return:
        '''
        func = (builtIn_func if ('_' in builtIn_func) else '_'.join(builtIn_func.split(' ')))
        try:
            function = getattr(BuiltIn(), func.lower())
            function(first, second)
        except Exception as e:
            raise Exception(e, err_msg)
        else:
            logger.info(expect_msg)

    @keyword(name="ZDownloadApk")
    @exception_handler()
    def download_apk(self, download_url, apk_name=None):
        """
        下载apk
        Examples:
            | ${path} | ZDownloadApk | https://cos.iwhalecloud.com/cos-ebdg-x/MOA/MOA_9.0.0.35-beta.apk |
        :param download_url: 下载地址
        :param apk_name: 保存的apk名，可不传，不传默认存成download_url中的apk名
        :return: 下载的apk包绝对路径
        """
        from Lib.zcm.app.app_service import download_apk
        return download_apk(download_url, apk_name)

    @keyword(name="ZInstallApk")
    @exception_handler()
    def install_apk(self, apk_name):
        """
        安装apk
        Examples:
            | ZInstallApk | MOA_9.0.0.35-beta.apk |
        :param apk_name: apk包路径。传入相对路径时，会到/home/seluser/testproject/Case/apk_package下寻找
        """
        from Lib.zcm.app.app_service import install_apk
        return install_apk(apk_name)

    @keyword(name="ZCompareWithinValues")
    def compare_within_values(self, value_one, value_two):
        """
        两值比较, vaue_one大于value_two返回True, 否则返回false
        :param value_one:
        :param value_two:
        :return:
        """
        flag = False
        if value_one > value_two:
            flag = True
        return flag

    @keyword(name="ZGetJsonObject")
    def get_json_object(self, json_string):
        """把字符串转成json格式
        Examples:
            | ${json} | ZGetJsonObject |  {"a":"b"} |
        """
        return JSONLibrary().convert_string_to_json(json_string)

    @keyword(name="ZJsonToString")
    def get_json_string(self, json_obj):
        """把json对像转成字符串格式
        Examples:
            | ${json_str} | ZJsonToString | ${json} |
        """

        return json.dumps(json_obj, cls=DecimalEncoder)
        # return JSONLibrary().convert_json_to_string(json)

    @keyword(name="ZAddToJson")
    def json_add(self, json, json_path, object_to_add):
        """向Json对像里添加内容
        Examples:
            | ${new_json} | ZAddToJson | ${json} | $.add_path | value |
        参数：
        - json: json对像
        - json_path: JSONPath, 具体语法参考ZJsonGetValue关键字
        - object_to_add: 要添加的内容
        """
        return JSONLibrary().add_object_to_json(json, json_path, object_to_add)

    @keyword(name="ZJsonLoadFromFile")
    def json_load_file(self, file_name):
        """读取文件内容并转成json对像
        Examples:
            | ${json} | ZJsonLoadFromFile | c:\my.json |
        参数：
        - file_name: 文件路径
        """
        # return JSONLibrary().load_json_from_file(file_name)
        with open(file_name, encoding='utf-8') as json_file:
            data = json.load(json_file)
        return data

    @keyword(name="ZJsonUpdateValue")
    def json_update(self, json, *params):
        """更新Json对像内容,可以支持批量更新,当由于jsonPath指定的不存在，不报错
        Examples:
            | ${new_json} | ZJsonUpdateValue | ${json} | $.update_path | value |
        参数：
        - json: json对像
        - json_path: JSONPath, 具体语法参考ZJsonGetValue关键字
        - object_to_update: 要更新的值
        """
        count = len(params)
        if count % 2 != 0:
            raise TestException.by_error_code(ErrorCode.PARAM_ERROR,
                                              message="json_path and object to update are not matched!")
        index = 0
        result = json
        while index < count:
            result = JSONLibrary().update_value_to_json(result, params[index], params[index + 1])
            index += 2
        return result

    @keyword(name="ZJsonGetValues")
    def json_values(self, json, json_path):
        """获取Json对像的指定json_path的值，返回多个
        Examples:
            | ${values} | ZJsonGetValues | ${json} | $.path |
        参数：
        - json: json对像
        - json_path: JSONPath, 具体语法参考ZJsonGetValue关键字
        - return: 数组
        """
        return JSONLibrary().get_value_from_json(json, json_path)

    @keyword(name="ZJsonGetValue")
    def json_value(self, json, json_path):
        """获取Json对像的指定json_path的值，返回找到的第一个
        Examples:
            | ${value} | ZJsonGetValue | ${json} | $.path |
        参数：
        - json: json对像
        - json_path: JSONPath, 具体语法参考如下

        == JSONPath Syntax ==
        | JSONPath | Description |
        | $        | the root object/element |
        | @        | the current object/element |
        | . or []  | child operator |
        | ..       | recursive descent. JSONPath borrows this syntax from E4X |
        | *        | wildcard. All objects/element regardless their names. |
        | []       | subscript operator. XPath uses it to iterate over element collections and for predicates. In Javascript and JSON it is the native array operator. |
        | [,]      | Union operator in XPath results in a combination of node sets. JSONPath allows alternate names or array indices as a set. |
        | [start:end:step] | array slice operator borrowed from ES4 |
        | ?()      | applies a filter (script) expression. |
        | ()       | script expression, using the underlying script engine. |

        == *** example *** ==
        比如示例json为：
        {"a": "a_value",\n
         "b": {\n
             "b_a": "b_a_value",\n
             "b_b": [\n
                 {"b_b_a": "b_b_a_value"},\n
                 {"b_b_b": "b_b_b_value"}\n
                ]\n
            }\n
        }\n

        | JSONPath | 取到的值 |
        | $.b.b_a | b_a_value |
        | $..b_a  | b_a_value |
        | $..b_b[1].b_b_b | b_b_b_value |
        """
        result = JSONLibrary().get_value_from_json(json, json_path)
        if result and len(result) > 0:
            return result[0]
        else:
            return None

    @keyword(name="ZJsonDelete")
    def json_delete(self, json, json_path):
        """向Json对像里删除内容
        Examples:
            | ${new_json} | ZJsonDelete | ${json} | $.delete_path |
        参数：
        - json: json对像
        - json_path: JSONPath, 具体语法参考ZJsonGetValue关键字
        """
        return JSONLibrary().delete_object_from_json(json, json_path)

    @keyword(name="ZPrintBrowserLog")
    def log_browser(self):
        """输出浏览器日志
        """
        log = self._drivers.current.get_log('browser')
        logger.info(log)

    @keyword(name="ZGetConfig")
    def get_config(self, section_name, config_name, default=None):
        """读取Case目录下的 config.ini 的配置信息
        Example:
            | ${result} | ZGetConfig | GENERAL | version | 8.0 |
        参数：
        - section_name: 配置项段名
        - config_name: 配置项名
        - default: 如果没有这个配置项时返回的缺省值，默认为None
        - 返回读到的配置项的值
        """
        return FwVariables.get_value(section_name, config_name, default)

    @keyword(name="ZCheckZCMConfig")
    def check_zcm_config(self, rule_info, file_path=None, portal_domain_name=ZCM_PORTAL_ROOT_URL, project_id=None):
        """
        根据规则扫描配置是否符合规范
        Example:
            ${ignore_config}    Create Dictionary    cache.type=CtgCache
            ${rule_info}    Create Dictionary    project_name=iCICD研发管理    tenant=南研小机房    group_name=jhj-test    file_name=aa.properties    key_name=cache.type    comment_suggestion=该项目前已被注释掉，如不需要该项，直接用例中删除，就不会进行判断    correct_value=111111    config_suggestion=账号锁定之后多长时间能自动解锁（单位：分钟）    ignore_config=${ignore_config}
            #ZCheckZCMConfig    ${rule_info}    ./Case/test.xlsx
            ZCheckZCMConfig    ${rule_info}
        参数：
        - param rule_info: 配置信息和规则信息，格式为字典\n
            - param project_name: 项目名 (必填)\n
            - param tenant: 租户 (必填)\n
            - param group_name: 配置组名 (选填)\n
            - param file_name: 配置文件名 (选填)\n
            - param key_name: 配置项名 (选填)\n
            - param comment_suggestion: 注释建议 (选填)\n
            - param correct_value: 正确配置 (选填)\n
            - param config_suggestion: 配置建议 (选填)\n
            - param ignore_config: 忽略配置项，需传入忽略值，字典格式 (选填)。 key为特殊字符 "^*$" 时，表示全局忽略【谨慎使用！！】\n
            Example:
                {
                "project_name":"demo",
                "tenant":"南研小机房",
                "group_name":"arc_service_product",
                "file_name":"portalConfig.properties",
                "key_name":"app.security.enable-rbac",
                "comment_suggestion":"该项目前已被注释掉，如不需要该项，直接用例中删除，就不会进行判断。",
                "correct_value": "30",
                "config_suggestion":"账号锁定之后多长时间能自动解锁（单位：分钟）",
                "ignore_config": {"portalConfig.properties":{"app.security.enable-rbac":"40"}}
                }
        - param file_path: 规范excel文件路径,默认不传。当传入时扫描excel中配置项\n
        - param portal_domain_name: Portal域名，默认为所内域名：https://dev.iwhalecloud.com/\n
        - param project_id: 项目id，默认不传，当根据租户和项目名不能确定唯一project_id时，可手动传入\n
        """
        SafetyMonitor().check_zcm_config(rule_info, file_path, portal_domain_name, project_id)

    @keyword(name="ZCheckZCMConfigPwd")
    def check_zcm_config_pwd(self, project_name, tenant, ignore_config=dict(), portal_domain_name=ZCM_PORTAL_ROOT_URL,
                             project_id=None):
        """
        扫描传入的项目、租户下的所有properties文件密码是否加密
        Example:
            ${key_list}    Create List    ctgcache.password.20    ftf.datasource.druid.password
            ${ignore_list}    Create Dictionary    aa.properties=${key_list}
            ZCheckZCMConfigPwd    demo    南研小机房    ${ignore_list}
        - param project_name: 项目名 (必填)\n
        - param tenant: 租户 (必填)\n
        - param ignore_config: 忽略配置项，字典形式 (选填)\n
        - param portal_domain_name: Portal域名，默认为所内域名：https://dev.iwhalecloud.com/ (选填)\n
        - param project_id: 项目id，默认不传，当根据租户和项目名不能确定唯一project_id时，可手动传入 (选填)\n
        """
        SafetyMonitor().check_zcm_config_pwd(project_name, tenant, ignore_config, portal_domain_name, project_id)

    @keyword(name="ZDubboCall")
    @exception_handler()
    def dubbo_call(self, interface, method, args=None, host=None):
        """【即将废弃！！请调用ZDubboTest关键字】dubbo接口调用(Dubbo需要2.0.5版本以上)

        Examples:
            | ${params} | Create Dictionary | id=2 | page=1 |
            | ${result} | ZDubboCall | com.ztesoft.zsmart.bss.ar.external.api.IBillQry4PaymentService | setCustData | ${params} |
        - param interface: 接口\n
        - param method: 方法名\n
        - param args: 调用方法的参数，格式参见下面说明\n
        - param host: 如果传入服务器的地址，格式ip:port, 如果不传，则使用配置文件中config.ini中的zookeeper配置
        - return: 调用返回的消息，字符串格式\n

        **config.ini的配置**

        在Case根目录的config.ini加上DUBBO_CLIENT段配置：\n
        [DUBBO_CLIENT]\n
        zookeeper = ip:port # zookeeper的地址， 如果不配置，dubbo_call必需带host参数，指定接口地址\n
        timeout = 300 # 请求超时时间，默认300s\n
        registry_group = GLOBAL_REGISTRY # 注删服务的group，默认dubbo

        **调用方法参数的格式**
        - 如果是基础的数据类型（数字，字符传），可以直接传，如 1,"2"
        - 如果是普通的类型，传json，如{"param1":"value1","param2":"value2"}
        - 如果是java对像，需要在json对像中加class字段指出对像类型名，如 {"key":"11","merchantId":"888","class":"com.cn.Test"}
        具体可参数dbuuo telnet协议
        """
        response = Dubbo_Client.request(interface, method, args, host)
        return response.text

    @keyword(name="ZDubboTest")
    @exception_handler()
    def dubbo_test(self, link_name, interface, method, *args):
        """
        dubbo接口调用
        【研发平台-云测中心-dubbo测试，可以进行dubbo接口的测试，并提供自动生成dubbo测试用例，可直接复制到你的用例中，无需自行编写用例】

        Examples:
            | &{params} = | Create Dictionary | paramType=com.ztesoft.bss.prod.cust.service.param.UpdatePostpaidCreditInfoReq | paramValue={"custId":202194,"fpValue":null,"fpType":null,"fpTypeName":null,"maxMonthlyFee":null,"maxCreditLimit":null,"effDate":null,"expDate":null,"staffId":null,"orgId":null,"postId":null,"oldCustOrderId":null,"acctCreditInfos":[{"acctId":249497,"tmpCreditLimit":null,"creditLimit":2600000000,"effDate":1646968256000,"expDate":2492956800000,"needApproveFlag":"N","remark":"333","ccType":2}]} |
            | ${result} | ZDubboCall | DUBBO_CLIENT | com.ztesoft.bss.prod.cust.service.CustomerServiceExt | updatePostpaidCreditInfo | ${params} |
        - param link_name: config.ini中配置的数据section名称
        - param interface: 接口
        - param method: 方法名
        - param args: 调用方法的参数(字典类型)
            - paramType: 参数类型
            - paramValue: 参数值

        **config.ini的配置**

        在Case根目录的config.ini加上DUBBO_CLIENT(section名称可自定义)段配置：\n
        [DUBBO_CLIENT]\n
        zookeeper = ip:port # zookeeper的地址\n
        timeout = 300 # 请求超时时间，默认300s\n
        registry_group = GLOBAL_REGISTRY # 注删服务的group \n
        """
        response = Dubbo_Test_Client.request(link_name, interface, method, *args)
        return response.text

    @keyword(name="ZHttpRunTest")
    @exception_handler()
    def http_run_test_case(self, case_file, params: dict = None, update_bodys: dict = None, headers: dict = None,
                           cookies: dict = None):
        """ 执行HttpRun测试
        - case_file: 测试用例Json定义文件，格式详见 https://docs.httprunner.org/user/gen_tests/#generate-testcase-yamljson
        - params: 请求参数，字典类型
        - update_bodys: 要修改的请求POST的消息内容，字典类型，json_path=value, json_path的格式可以参照 ``ZJsonGetValue`` 关键字的json_path说明
        - headers: HTTP消息Header头，字典类型,如 User-Agent=ride
        - cookies: HTTP消息的的Cookies, 字典类型，如 session=23432
        - 【RETURN】：返回接口消息内容

        注：
        - 获取HTTP消息响应码：${_HTTP_RESPONSE.status_code}
        - 获取响应消息的Header头中的Connection值：${_HTTP_RESPONSE.headers['Connection']}

        api address优先级:
        1. case中配置的${SWAGGER_BASE_URL}
        2. config文件中配置的openapi
        3. template中的默认地址
        """
        from Lib.zcm.utils.http_runner_help import HttpRunnerHelp
        from httprunner.loader import load_testcase_file
        from httprunner import HttpRunner

        # 加载测试用例Json
        testcase_obj = load_testcase_file(case_file)

        # 获取config中配置的openapi地址, 未配置默认为None
        open_api = FwVariables.get_api_addr()
        if open_api:
            config_params = {"BASE_URL": open_api}
            # 更新config的variable,此变量用于url路径
            for key in config_params.keys():
                if key in testcase_obj.config.variables.keys():
                    testcase_obj.config.variables.update({key: config_params[key]})
        # 参数化
        if params:
            # 更新config的variable,此变量用于url路径
            for key in list(params.keys()):
                if key in testcase_obj.config.variables.keys() and params[key]:
                    v = params.pop(key)
                    testcase_obj.config.variables.update({key: v})
        request_cookies, requests_headers = http_interface_obj._get_http_cookie_and_headers(None, None)
        requests_headers = self.update_case_info_header_to(requests_headers)
        for step in testcase_obj.teststeps:
            if params:
                step.request.params.update(params)
            # 看是更新data还是req_json
            if update_bodys and step.request.data:
                if isinstance(step.request.data, str):
                    step.request.data = update_bodys
                elif isinstance(step.request.data, dict):
                    step.request.data.update(update_bodys)
            if update_bodys and step.request.req_json:
                new_json = step.request.req_json
                for key, value in update_bodys.items():
                    new_json = self.json_update(new_json, key, value)
                logger.info(new_json)
                step.request.req_json = new_json
            # 上一次保存的Header
            if requests_headers:
                step.request.headers.update(headers)
            if headers:
                step.request.headers.update(headers)
            # 上一次消息保存的cookies
            if request_cookies:
                step.request.cookies.update(request_cookies)
            if cookies:
                step.request.cookies.update(cookies)
        # 发送消息
        logger.debug("TESTCASE OBJECT:")
        logger.debug(testcase_obj)
        # 调HttpRunner进行接口测试
        runner = HttpRunner()
        result = runner.run_testcase(testcase_obj)

        # 打印
        try:
            for step in testcase_obj.teststeps:
                step_response = step.variables["response"]
                HttpRunnerHelp.log_req_resp_record(step_response)
        except:
            traceback.print_exc()
            pass

        # 得到最后一次接口响应消息，一般一个用例就一个步骤
        response_obj = testcase_obj.teststeps[-1].variables["response"]
        # 保存消息
        http_interface_obj._save_http_cookie_and_headers(response_obj)

        # 判断HttpRunner用例是否执行成功，还得获取下错误信息
        if not result.success:
            raise TestException.by_error_code(ErrorCode.HTTP_TEST_FAILED, message="Please view detail log message.")
        if not get_ignore_error():
            # 判断响应码，不是200则认为失败
            if response_obj.status_code < 200 or response_obj.status_code >= 300:
                raise TestException.by_error_code(ErrorCode.HTTP_TEST_FAILED,
                                                  message=f"Response Status Code {response_obj.status_code}")
        # 返回接口响应内容，用户可以再进一步校验
        return response_obj.text

    @keyword(name="ZExcelRowAndCol")
    @exception_handler()
    def excel_row_col(self, file_path, sheet_name='Sheet1'):
        """ 返回Excel表格的最大行和最大列

        Example:
            | ${row} | ${col} | ZExcelRowAndCol | c:\\demo.xls |

        - file_path: excel文件的路径
        - sheet_name: sheet名字，默认Sheet1
        - 【RETURN】：最大行，最大列
        """
        excel = ExcelOpts(file_path, sheet_name)
        return excel._calc_rows(), excel._calc_cols()

    @keyword(name="ZExcelRead")
    @exception_handler()
    def excel_read(self, file_path, row, start_col=0, end_col=None, sheet_name='Sheet1'):
        """ 读excel文件内容

        Example:
            | ${values} | ZExcelRead | c:\\demo.xls | 4 | # 读单元格第5行数据
            | ${values} | ZExcelRead | c:\\demo.xls | 4 | 3 | 4| # 读单元格第5行数据，第4例数据

        - file_path: excel文件的路径
        - row: 第几行，从0行开始
        - start_col: 第几例开始：默认从0例开始，需要大于等于0
        - end_col: 读到第几例，默认None，表示读到最后一例，不能大于最后一例
        - sheet_name: sheet名字，默认Sheet1
        - 【RETURN】：返回指定单元格的内容数据
        """
        excel = ExcelOpts(file_path, sheet_name)
        return excel.read(int(row), int(start_col), int(end_col) if end_col else end_col)

    @keyword(name="ZExcelWrite")
    @exception_handler()
    def excel_write(self, file_path, row, col, value, sheet_name='Sheet1'):
        """ 写excel文件

        Example:
            | ZExcelWrite | c:\\demo.xls | 4 | 1 | 写入的值 |

        - file_path: excel文件的路径
        - row: 第几行，从0行开始
        - col: 第几列：从0例开始
        - value: 要写入的值
        - sheet_name: sheet名字，默认Sheet1
        """
        excel = ExcelOpts(file_path, sheet_name)
        excel.write(int(row), int(col), value)
        excel.save()

    @keyword(name="ZLogFile")
    def log_file(self, message, file_path=None, is_clean=False):
        """追加写日志文件

        Example:
            | ZLogFile | 要写入的消息 | 日志文件路径 |

        - message: 要写入的消息
        - file_path: 日志文件路径，不传默认是Case目录下case.log
        - is_clean: 是否清楚之前日志
        """
        from Lib.zcm.assistant import FwConfigUtils

        log_file = file_path or os.path.join(FwConfigUtils.root_path, "Case", "case.log")

        try:
            file_path, _ = os.path.split(log_file)
            if file_path and not os.path.exists(file_path):
                os.makedirs(file_path)
            method = "w" if is_clean else "a"
            with open(log_file, method, encoding="utf8") as f:
                f.write(str(message))
                f.write("\n")
            logger.info(f"log print into file {log_file}")
        except Exception as e:
            # 写入报错，不至于中断测试
            logger.error(f"Write log file {log_file} error: {str(e)}")

    @keyword(name="ZGetListDir")
    def get_list_dir(self, file_path):
        """
        逆序返回dir下所有文件, 以list形式返回
        :param file_path:
        :return: list
        """
        dir_list = os.listdir(file_path)
        dir_list = sorted(dir_list, key=lambda x: os.path.getmtime(os.path.join(file_path, x)), reverse=True)
        return dir_list

    @keyword(name="ZDelLocalFile")
    def delete_local_file(self, file_path):
        """
        删除本地文件
        :param file_path:
        :return:
        """
        try:
            os.remove(file_path)
        except OSError as e:
            logger.debug(e)
            dir_list = os.listdir(file_path)
            if dir_list:
                for file in dir_list:
                    os.remove(os.path.join(file_path, file))
            # os.rmdir(file_path)

    @keyword(name="ZContainerExecCommand")
    @exception_handler()
    def container_exec_command(self, project_name, application_name, command):
        """
        telnet到容器执行命令

        Example:
            | ZContainerExecCommand | demo | appname | ls |

        - project_name: 项目名称
        - application_name: 应用名称
        - command: 命令行
        """
        logger.info(command)
        controller = Pod_Controller(project_name, application_name)
        return controller.exec_command(command)

    @keyword(name="ZContainerDownloadFile")
    @exception_handler()
    def container_download_file(self, project_name, application_name, remote_file_path, local_file_path):
        """下载ZCM项目应用下的文件

        Example:
            | ZContainerDownloadFile | demo | appname | /etc/conf/super.conf | d:\\project\\Case\\super.conf |

        - project_name: 项目名称
        - application_name: 应用名称
        - remote_file_path: 远程文件的路径
        - local_file_path: 本地保存文件的路径
        """
        logger.info(remote_file_path)
        controller = Pod_Controller(project_name, application_name)
        controller.download_file(remote_file_path, local_file_path)

    @keyword(name="ZContainerUploadFile")
    @exception_handler()
    def container_upload_file(self, project_name, application_name, local_file_path, remote_path):
        """上传ZCM项目应用文件

        Example:
            | ZContainerUploadFile | demo | appname | d:\\project\\Case\\super.conf | /etc/conf/ |

        - project_name: 项目名称
        - application_name: 应用名称
        - local_file_path: 本地要上传的文件路径
        - local_file_path: 文件保存到远程服务器的路径,不带文件名，直接使用本地文件的文件名
        """
        controller = Pod_Controller(project_name, application_name)
        controller.upload_file(local_file_path, remote_path)

    @keyword(name="ZSendDccMessage")
    @exception_handler()
    def send_dcc_message(self, acc_nbr, service_type=None, cdr_attr=None):
        """发送DCC协议消息，不获取返回消息

        DCC的配置文件路径可以在**config.ini**中配置\n
        在Case根目录的config.ini加上DUBBO_CLIENT段配置：\n
        [DCC]\n
        config_path =  # 如果不配置, 则采用Lib库中的默认配置\n
        """
        from Lib.zcm.dcc.dcc_request import DccRequest
        with DccRequest() as req:
            req.send_dcc_message(acc_nbr, service_type, cdr_attr)

    @keyword(name="ZDccConnect")
    @exception_handler()
    def send_dcc(self, olc_link_name):
        """发送DCC协议消息，并校验返回的消息

        olc_link_name: config.ini中配置的olc的section名称
        avp_type: 对应的avpfile的配置参数
        request_dict: 请求包（字典形式）
        check_json: 校验参数（字典形式）
        """
        from Lib.zcm.dcc.dcc_request import dccOperate
        return dccOperate.connect()

    @keyword(name="ZSendDccMsg")
    @exception_handler()
    def send_dcc(self, olc_link_name, avp_type, request_dict, check_dict=None):
        """发送DCC协议消息，并校验返回的消息

        dcc_sock: socket连接
        avp_type: 对应的avpfile的配置参数
        request_dict: 请求包（字典形式）
        check_dict: 校验参数（字典形式）, 传空或不传时，表示不校验
        """
        from Lib.zcm.dcc.dcc_request import dccOperate
        return dccOperate.send_dcc_message(olc_link_name, avp_type, request_dict, check_dict)

    @keyword(name="ZGetDccOriginHost")
    @exception_handler()
    def get_origin_host(self):
        """发送DCC协议消息，并校验返回的消息

        dcc_sock: socket连接
        avp_type: 对应的avpfile的配置参数
        request_dict: 请求包（字典形式）
        check_json: 校验参数（字典形式）


        """
        return FwVariables.get_origin_host()

    @keyword(name="ZTransforAvpFileToJson")
    @exception_handler()
    def transfor_avpfile_to_json(self, avp_type):
        from Lib.zcm.dcc.dcc_request import dccOperate
        return dccOperate.transfor_avpfile_to_json(avp_type)

    @keyword(name="ZSendDccMessageWithResponse")
    @exception_handler()
    def send_dcc_message_with_response(self, acc_nbr, service_type=None, cdr_attr=None, timeout=30):
        """发送DCC协议消息，获取返回消息

        DCC的配置文件路径可以在**config.ini**中配置\n
        在Case根目录的config.ini加上DUBBO_CLIENT段配置：\n
        [DCC]\n
        config_path =  # 如果不配置, 则采用Lib库中的默认配置\n
        """
        from Lib.zcm.dcc.dcc_request import DccRequest
        with DccRequest() as req:
            return req.send_dcc_message_with_response(acc_nbr, service_type, cdr_attr)

    @keyword(name="ZGetPageConfig")
    @exception_handler()
    def read_page_config(self, element_name, page_name='DEFAULT'):
        """获取Page配置的元素定位字符串，如果没有读取到，则返回None

        - element_name: 元素名称，Page配置里定义的
        - page_name: 页面名称，对应Page配置里的Section，默认是DEFAULT
        """
        from Lib.zcm.assistant.page_config import page_parser
        val = None
        try:
            val = page_parser.read_config(page_name, element_name)
        except TestException as e:
            logger.error(e)
        return val

    @keyword(name="ZSftpUpload")
    @exception_handler()
    def sftp_upload(self, link_name, local_path, remote_path, **kwargs):
        """
        sftp 上传

        - link_name: config.ini中配置的olc的section名称
        - local_path： 本地路径
        - remote_path: sftp路径

        Example:
            | ZSftpUpload | sftp | D:\\sftp\\1.xls | /home/ocs90/sftp/2.xls |
            | ZSftpUpload | sftp | D:\\sftp | /home/ocs90 |
        """
        sftp_obj.sftp_upload(link_name, local_path, remote_path, **kwargs)

    @keyword(name="ZSftpDownload")
    @exception_handler()
    def sftp_download(self, link_name, local_path, remote_path, **kwargs):
        """
        sftp 下载

        - link_name: config.ini中配置的olc的section名称
        - local_path： 本地路径
        - remote_path: sftp路径

        Example:
            | ZSftpDownload | sftp | D:\\ftp\\xx.xls | /home/ocs90/sftp/2.xls  |
        """
        sftp_obj.sftp_download(link_name, local_path, remote_path, **kwargs)

    @keyword(name="ZExeRemoteCommand")
    @exception_handler()
    def exec_remote_command(self, link_name, command_info, wait_time=2):
        """
        远程主机并执行指令
        - link_name: config.ini中配置的olc的section名称
        - command_info： 远程要执行的指令信息，字典格式，key为执行的指令，value为需要交互的指令，如无需交互，传入空列表
        - wait_time: 命令执行之间等待时间

        Example:
            | ${inputs} | Create List | 123 | 456 | \x03 |
            | ${command_info} | Create Dictionary | 3= | sudo sudo -i= | /zpaas/namesrv/zmq/bin/mqadmin producer -t testxw=${inputs} |
            | ${result} | ZExeRemoteCommand | ZMQ | ${command_info} |
            | ZStep | ${result} |
        """
        return execute_remote_command(link_name, command_info, wait_time)

    @keyword(name="ZRedisConnect")
    @exception_handler()
    def redis_connect(self, startup_nodes=None, **kwargs):
        """连接redis 集群
        示例:
            | ${redis_host1} | CreateDictionary | host=192.154.3.3 | port=8686 |
            | ${redis_host2} | CreateDictionary | host=192.154.3.4 | port=8686 |
            | ${start_nodes} | Create List | ${redis_host1} | ${redis_host2} |
            | ${redis}= | ZRedisConnect | ${start_nodes} | |
        """
        from Lib.zcm.utils.redis_client import redis_connect
        return redis_connect(startup_nodes, **kwargs)

    @keyword(name="ZRedisDisConnect")
    @exception_handler()
    def redis_disconnect(self, redis):
        """
        断开redis连接
        示例:
            | ZRedisDisConnect | ${redis} |
        """
        from Lib.zcm.utils.redis_client import redis_disconnect
        return redis_disconnect(redis)

    @keyword(name="ZRedisSet")
    @exception_handler()
    def redis_set(self, redis, name, value):
        """设置redis值
        示例:
            | ${redis}= | ZRedisConnect | ${start_nodes} | |
            | ZRedisSet | ${redis} | name | value |
        """
        redis.set(name, value)

    @keyword(name="ZRedisGet")
    @exception_handler()
    def redis_get(self, redis, name):
        """设置redis值
        示例:
            | ${redis}= | ZRedisConnect | ${start_nodes} | |
            | ${value}= | ZRedisGet | ${redis} | name |  |
        """
        return redis.get(name)

    @keyword(name="ZRedisHashSet")
    @exception_handler()
    def redis_hset(self, redis, name, key, value):
        """设置redis值的hash类型值
        示例:
            | ${redis}= | ZRedisConnect | ${start_nodes} | |
            | ZRedisHashSet | ${redis} | name | key| value |
        """
        return redis.hset(name, key, value)

    @keyword(name="ZRedisHashGet")
    @exception_handler()
    def redis_hget(self, redis, name, key):
        """获取redis的hash类型值
        示例:
            | ${redis}= | ZRedisConnect | ${start_nodes} | |
            | ${value}= | ZRedisHashGet | ${redis} | name | key |
        """
        return redis.hget(name, key)

    @keyword(name="ZRedisHashGetAll")
    @exception_handler()
    def redis_hgetall(self, redis, name):
        """返回redis的所有hash值，多个的话为列表key:value形式
        示例:
            | ${redis}= | ZRedisConnect | ${start_nodes} | |
            | ${value}= | ZRedisHashGetAll | ${redis} | name |
        """
        return redis.hgetall(name)

    @keyword(name="ZRedisHashKeys")
    @exception_handler()
    def redis_hkeys(self, redis, name):
        """返回redis的所有hash所有key值
        示例:
            | ${redis}= | ZRedisConnect | ${start_nodes} | |
            | ${keys}= | ZRedisHashKeys | ${redis} | name |
        """
        return redis.hkeys(name)

    @keyword(name="ZRedisCommand")
    @exception_handler()
    def redis_command(self, redis, method, *args, **kwargs):
        """执行redis的方法,常用的方法有get,set,hget,hset,hkeys,hgetall,getset,delete,hexists...
        示例:
            | ${redis}= | ZRedisConnect | ${start_nodes} | |
            | ${value}= | ZRedisCommand | ${redis} | get | foo |
        """
        if not hasattr(redis, method):
            raise TestException.by_error_code(ErrorCode.PARAM_ERROR, message=f"Redis has not {method} method!")

        func = getattr(redis, method)
        return func(*args, **kwargs)

    @keyword(name="ZVerificationCode")
    @exception_handler()
    @capture_screen_handler()
    def verification_code_recognition(self, element, excepted_count=-1, image_type="number", ocr_driver="baidu"):
        """验证码识别，
        - element: 界面上显示验证码的那个图片元素
        - excepted_count: 验证码的位数，如果与识别到的数量不相符，则成功返回False，-1代码不管位数
        - image_type: 验证码类型，包括：number-数字(默认），general-通用类型，包括数字，字母等
        - ocr_driver: 识别引擎： 当前支持：baidu,ttshitu,local
        - 【返回】： 识别结果，是否成功

        示例：
            | ${code} |	${is_ok} | ZVerificationCode | css=img.codeCon |
        """
        from Lib.zcm.utils.ocr import BaiduOCR, ttshituOCR, LocalOCR
        if isinstance(element, str):
            element = Common().get_element_by_selector(self._drivers.current, element)

        web_element = element._get_web_element()
        image_base64 = web_element.screenshot_as_base64
        if ocr_driver == "baidu":
            ocr = BaiduOCR()
        elif ocr_driver == "ttshitu":
            ocr = ttshituOCR()
        elif ocr_driver == "local":
            ocr = LocalOCR()
        else:
            raise TestException.by_error_code(ErrorCode.PARAM_ERROR, message=f"OCR not support {ocr_driver} driver")
        # 转成数字
        excepted_count = int(excepted_count)
        if image_type == "number":
            return ocr.ocr_number_base64(image_base64, excepted_count)
        elif image_type == "general":
            return ocr.ocr_general_base64(image_base64, excepted_count)
        else:
            raise TestException.by_error_code(ErrorCode.PARAM_ERROR, message=f"OCR not support {image_type} image type")

    @keyword(name="ZEncryptString")
    @exception_handler()
    def encrypt_string(self, message, type=None):
        """加密字符串，
        - message: 字符串明文
        - type: 加密算法，默认rsa_cloud_test，不传就是rsa_cloud_test
        - 【返回】： 返回密文

        示例：
            | ${result} | ZEncryptString | hello |
        """
        from Lib.zcm.utils.encryptor import EncryptUtil
        # 需要安装ras类库

        result = EncryptUtil.encrypt_string(message, type)
        logger.info("encrypt result: " + result)
        return result

    @keyword(name="ZChooseFile")
    @exception_handler()
    @capture_screen_handler()
    def redefine_choose_file(self, locator, file_path):
        with WaitPageLoad(self._drivers.current):
            FormElementKeywords(self).choose_file(locator, file_path)

    @keyword(name="ZHttpRunPostManTest")
    @exception_handler()
    def http_run_postman_case(self, postman_case_file, params: dict = None, update_bodys: dict = None,
                              headers: dict = None,
                              cookies: dict = None):
        """ 根据导出的PostMan的用例文件执行接口测试

        示例：
             | ZHttpRunPostManTest | ${EXECDIR}${/}Case${/}data${/}Postman Echo.postman_collection.json |

        :参数：
            - postman_case_file: postman工具导出的测试用例文件，Collection V2.1格式的json文件路径
            - 其它参数： 参见ZHttpRunTest关键字说明
        :备注: HttpRunner BUG，消息中用中文会报编码错误 https://github.com/HttpRunner/HttpRunner/issues/614
        """
        from Lib.zcm.utils.postman2case.core import PostmanParser
        # 把postman用例文件转成httpruner格式
        try:
            postman_parser = PostmanParser(postman_case_file)
            parse_result = postman_parser.parse_data()
        except Exception as e:
            logger.error("parse postman export file failed")
            raise e
        file_paths = postman_parser.save(parse_result, "temp_json_file")
        for file_path in file_paths:
            self.http_run_test_case(file_path, params, update_bodys, headers, cookies)

    @keyword(name="ZHttpRunHarTest")
    @exception_handler()
    def http_run_har_case(self, har_case_file, params: dict = None, update_bodys: dict = None, headers: dict = None,
                          cookies: dict = None):
        """ 根据har文件执行接口测试

        示例：
             | ZHttpRunHarTest | ${EXECDIR}${/}Case${/}data${/}test.har |

        :参数：
            - har_case_file: 导出的消息交互的har文件路径
            - 其它参数： 参见ZHttpRunTest关键字
        """
        from Lib.zcm.utils.postman2case.harparser import MyHarParser

        harfile = os.path.splitext(har_case_file)[0]
        output_testcase_file = f"{harfile}.json"

        har_parser = MyHarParser(har_case_file)
        har_parser.gen_testcase("JSON")
        # 有Bug，生成的文件中如果带-，执行会报解析错误 https://github.com/httprunner/httprunner/issues/1036
        # 另一个BUG，消息中如果有中文，会报编码错误 https://github.com/HttpRunner/HttpRunner/issues/614
        self.http_run_test_case(output_testcase_file, params, update_bodys, headers, cookies)

    def update_case_info_header_to(self, headers):
        """
        Http请求中增加Header信息
        """
        if headers is None:
            headers = {}
        headers.update(self.get_case_info_header())
        return headers

    def get_case_info_header(self):
        """
        获取包含Case信息Header
        """
        header_key = BuiltIn().get_variable_value(FwVariables.case_http_proxy_header_key_variable(), "")
        header_value = BuiltIn().get_variable_value(FwVariables.case_http_proxy_header_value_variable(), "")
        if header_key and header_value:
            return {header_key: header_value}
        return {}

    def add_http_proxy_header(self, case: TestCase = None):
        """
        http代理注入统一heaer头
        """
        if case is None:
            header_value = 'zcm-autotest-' + str(FwVariables.get_task_id())
        else:
            header_value = 'zcm-autotest-' + str(FwVariables.get_task_id()) + "-" + case.longname
        header_dict = {FwVariables.case_http_proxy_header_key_variable_value(): header_value}
        logger.info("add headers {}".format(header_dict))
        proxy_add_header(header_dict)
        # 注入变量，方便用户需要自行注入HttpHeader标识
        if case is None:
            BuiltIn().set_suite_variable(FwVariables.case_http_proxy_header_key_variable(),
                                         FwVariables.case_http_proxy_header_key_variable_value())
            BuiltIn().set_suite_variable(FwVariables.case_http_proxy_header_value_variable(), header_value)
        else:
            BuiltIn().set_test_variable(FwVariables.case_http_proxy_header_key_variable(),
                                        FwVariables.case_http_proxy_header_key_variable_value())
            BuiltIn().set_test_variable(FwVariables.case_http_proxy_header_value_variable(), header_value)

    def log_har(self, har, caseResult: TestCaseResult, cost_time_thread):
        itracing_trace_id = None
        log_request_list = []
        for entry in har['log']['entries']:
            cost_time = entry['time']
            status = entry['response']['status']
            log_level = self.log_level(cost_time, status, cost_time_thread)
            if log_level != 'debug':
                for header in entry['response']['headers']:
                    if header['name'] == 'ITRACING_TRACE_ID':
                        itracing_trace_id = header['value']
                        break
                log_request_list.append({'log_level': log_level,
                                         'msg': "cost time [{cost}ms], status code [{status}], itracing_id [{itracing_id}], request [{url}]"
                                        .format(cost=cost_time, itracing_id=itracing_trace_id, status=status,
                                                url=entry['request']['url'])})
        if len(log_request_list) != 0:
            logger.warn("================Case [{name}] longname [{longname}] id [#{id}]".format(name=caseResult.name,
                                                                                                longname=caseResult.longname,
                                                                                                id=caseResult.id))
            for log_request in log_request_list:
                log_fun = getattr(logger, log_request['log_level'], None)
                if log_fun is not None:
                    log_fun(log_request['msg'])

    def log_level(self, cost_time, status, cost_time_thread):
        if status < 200 or status > 299:
            return "error"
        elif int(cost_time) > cost_time_thread:
            return "warn"
        return "debug"
